﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace SpectationClient.SpectralTools {
    public class ENVI_HeaderParser {
        /// <summary>
        /// Remaining string of the input header
        /// </summary>
        private string rest = null;
        private List<String> errors = new List<string>();
        private Dictionary<String, String> TagValues = new Dictionary<string, string>();

        private int line = 0;
        public ENVI_HeaderParser() {
            
        }
        
        /// <summary>
        /// Parses the given header string.
        /// </summary>
        /// <returns>Dictionary with header tag as key and tag value strings.</returns>
        public Dictionary<String, String> parse(String header, out List<String> LineErrors) {
            this.rest = header;
            LineErrors = new List<string>();
            this.errors = LineErrors;
            this.TagValues.Clear();
            this.line = 1;
            
            eatWS();
            readENVI();
            while(this.rest.Length > 0) {
                gotoNextTagDefinition();
                readTagDefinition();
            }
            int t = this.errors.Count;

           
            return this.TagValues;
        }

      

        /// <summary>
        /// Reads and returns a string with patter {value1, value2, ..., value n}.
        /// Furthermore it checks wheter {, [ or ( brackets are closed properly.
        /// </summary>
        /// <returns>The checked value definition</returns>
        private String eatMultilineTagValue() {
            String l = lookAhead();
            
            if(l != "{") {
                this.addError("Awaiting \"{\""); 
                return null;
            }

            Regex brackets = new Regex(@"[(){}\[\]\n]", RegexOptions.Multiline);
            int roundBrackets = 0; //()
            int squareBrackets = 0; //[]
            int curlyBrackets = 0;  //{}

            String head = String.Empty;
            foreach(Match m in brackets.Matches(rest)){
                
                switch(m.Value) {
                    case "(": roundBrackets++; break;
                    case ")": roundBrackets--; break;
                    case "[": squareBrackets++; break;
                    case "]": squareBrackets--; break;
                    case "{": curlyBrackets++; break;
                    case "}": curlyBrackets--; break;
                    case "\n": this.line++; break;
                    default: throw new NotImplementedException();
                }

                //check for missplaced brackets
                if(roundBrackets < 0) this.errors.Add(String.Format("Line {0}: missplaced ')'", line));
                if(squareBrackets < 0) this.errors.Add(String.Format("Line {0}: missplaced ']'", line));
                if(curlyBrackets < 0) this.errors.Add(String.Format("Line {0}: missplaced '}'", line));
                if(roundBrackets < 0 || squareBrackets < 0 || curlyBrackets < 0) {
                    String sub = this.rest.Substring(0, m.Index+m.Length);
                    this.addError("Wrong bracket", this.line + this.countLines(sub));                    
                    return null;
                }
                if(curlyBrackets == 0) {
                    head = rest.Substring(0, m.Index + m.Length);
                    break;
                }

            }

            this.line += this.countLines(head);
            this.rest = rest.Remove(0, head.Length);
            head = Regex.Replace(head, "\n", " ");
            head = Regex.Replace(head, "\r", "");
            head = Regex.Replace(head, @"^\s*{\s*", "");
            head = Regex.Replace(head, @"\s*}\s*$", "");
            return head;
        }

        

        private void readENVI() {
            Regex r = new Regex(@"ENVI\s*");
            Match m = r.Match(this.rest);
            if(!m.Success) {
                this.errors.Add(String.Format("Line {0}: Missing word \"{ENVI}\" attributes header start", this.line));
                gotoNextLine();
            } else {
                remove(m.Value);
            }
        }

        /// <summary>
        /// Count the number of new-line characters.
        /// </summary>
        /// <param name="inputString"></param>
        /// <returns></returns>
        private int countLines(string inputString) {
            return Regex.Matches(inputString, @"\n", RegexOptions.Multiline).Count;
        }

        /// <summary>
        /// Reads a tag definition
        /// </summary>
        private void readTagDefinition() {
            if(this.rest.Length == 0) return;
            Regex rTag = new Regex(@"^\w+(\s+\w+)*(?=\s*=)", RegexOptions.Multiline);
            Match mName = rTag.Match(this.rest);
            if(mName.Success){
                
                String tagName = TrimAll(mName.Value).ToLower();
                String tagValue = null;
                this.remove(mName.Value);
                eat(@"\s*=\s*");
                if(isMatch(@"^\s*{")) eat(@"^\s*");
                String la = lookAhead();
                switch(la) {
                    case "\n": addError(String.Format("Avaiting value or begin of array like definition for meta tag \"{0}\".", tagName));
                               break;
                    case "{": tagValue = this.eatMultilineTagValue(); 
                        break;
                    default: tagValue = eat(@"[^\r\n]+"); break;
                }
                if(this.TagValues.Keys.Contains(tagName)){
                    addError(String.Format("Tag {0} defined more than once", tagName));
                }else{
                    this.TagValues.Add(tagName, tagValue);
                }
            }else{
                addError("Missing meta tag definition");
            }
            
        }

        /// <summary>
        /// Adds an error message to the error dictionary related to the recent line.
        /// </summary>
        /// <param name="errorText"></param>
        private void addError(String errorText) {
            this.addError(errorText, this.line);
        }

        /// <summary>
        /// Adds an error message to the error dictionary related to a certain line.
        /// </summary>
        /// <param name="errorText"></param>
        /// <param name="line"></param>
        private void addError(String errorText, int line){
            this.errors.Add(String.Format("Line {0}: {1}", line, errorText));
        }

        /// <summary>
        /// Remove a string form the beginning of the rest-string.
        /// </summary>
        /// <param name="toRemove"></param>
        private void remove(String toRemove) {
            this.rest = this.rest.Remove(0, toRemove.Length);
            this.line += this.countLines(toRemove);
        }

        /// <summary>
        /// Removes a certain number of characters from the rest-string.
        /// </summary>
        /// <param name="nCharacters"></param>
        private void remove(int nCharacters) {
            String toRemove = this.rest.Substring(0, nCharacters);
            this.remove(toRemove);
        }

        /// <summary>
        /// Matches a regex pattern and removes it from the beginning of rest-string. 
        /// In case the pattern does no match, an error will be added.
        /// </summary>
        /// <param name="patternToEat"></param>
        /// <returns></returns>
        private string eat(String patternToEat) {
            return this.eat(patternToEat, false);
        }

        /// <summary>
        /// Matches a regex pattern and removes it from the beginning of rest-string. 
        /// In case the pattern does no match and isOptional is set to false, an error will be added.
        /// </summary>
        /// <param name="patternToEat"></param>
        /// <param name="isOptional">default=false</param>
        /// <returns></returns>
        private string eat(String patternToEat, bool isOptional) {
            if(patternToEat[0] != '^') patternToEat = '^'+patternToEat;
            Regex re = new Regex(patternToEat);
            Match rm = re.Match(this.rest);
            String toReturn = String.Empty;
            if(rm.Success) {
                //remove from remaining string
                toReturn = rm.Value;
                remove(rm.Value);
            } else {
                if(!isOptional) {
                    this.errors.Add(String.Format("Line {0}: Could not read pattern \"{1}\"", this.line, patternToEat));
                }
            }
            return toReturn;
        }

        /// <summary>
        /// Removes white-space attributes the beginning of the recent line
        /// </summary>
        private void eatWS() {
            Regex ws = new Regex(@"^(\s|\0)+");
            Regex nl = new Regex(@"\n");
            Match m = ws.Match(this.rest);
            if(m.Success) {
                remove(m.Value);
            }
        }

        /// <summary>
        /// Returns true in case the global rest string matches the given pattern
        /// </summary>
        /// <param name="pattern">Regex pattern to match</param>
        /// <returns></returns>
        private bool isMatch(String pattern) {
            return Regex.IsMatch(this.rest, pattern);
        }

        /// <summary>
        /// Remove the current line and eats all following white-space.
        /// </summary>
        public void gotoNextLine() {
            eat(@"[^\n]*\n\s*");
        }

        /// <summary>
        /// Like standard string trim() function, but replaces in-beetween spaces as well.
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private String TrimAll(String s) {
            return Regex.Replace(s, @"\s+", " ", RegexOptions.Multiline).Trim();
        }

        /// <summary>
        /// Goes to the beginning of the next valide tag name. 
        /// </summary>
        public void gotoNextTagDefinition() {
            if(this.rest.Length == 0) return;
            Regex rTag = new Regex(@"^\s*(?=(\w+\s*)+=\s*)", RegexOptions.Multiline);
            Match m = rTag.Match(this.rest);
            this.remove(m.Value);
            eatWS();
            //String stop = "";
            //this.rest = this.rest.Substring(m.Index);
        }

        /// <summary>
        /// Returns the l=1 lookahead.
        /// </summary>
        /// <returns></returns>
        public string lookAhead() {
            return this.lookAhead(1);
        }

        /// <summary>
        /// Returns the first l characters form this.rest string with l less or equal the n of this.rest.
        /// </summary>
        /// <param name="width"></param>
        /// <returns></returns>
        public string lookAhead(int width) {
            return this.rest.Substring(0, width < this.rest.Length ? width : this.rest.Length );
        }

    }
}
